package com.zy.explore.web.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zy.explore.handler.ProjectExceptionHandler;
import lombok.val;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.server.ResponseStatusException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;

/**
 * @author Administrator
 */
@Order(99)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
public class SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    private final WrappedHandler handler;

    private final AuthProperties properties;

    @Nullable
    private final CorsConfigurationSource cors;

    public SecurityConfigurerAdapter(ObjectMapper mapper, ProjectExceptionHandler handler, AuthProperties properties, ObjectProvider<CorsConfigurationSource> cors) {
        this.handler = new WrappedHandler(mapper, handler);
        this.properties = properties;
        this.cors = cors.getIfAvailable();
    }

    @Override
    public void configure(WebSecurity web) {
        val resourcePaths = new ArrayList<>(getAuthProperties().getResourcePath());

        resourcePaths.add("/v2/api-docs");
        resourcePaths.add("/v2/api-docs-ext");
        resourcePaths.add("/swagger-resources/**");
        resourcePaths.add("/doc.html");
        resourcePaths.add("/webjars/**");

        web.ignoring().antMatchers(resourcePaths.toArray(new String[0]));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        configureHandlers(http);
        authorizeRequests(http.authorizeRequests());
        addFilters(http);
    }

    protected void configureHandlers(HttpSecurity http) throws Exception {
        val cors = getCorsConfigurationSource();

        if (cors == null) {
            http.cors().disable();
        } else {
            http.cors().configurationSource(cors)
                .and();
        }

        http.csrf().disable()
            .httpBasic().disable()
            .formLogin().disable()
            .logout().disable()
            .requestCache().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .exceptionHandling()
            .accessDeniedHandler(getAccessDeniedHandler())
            .authenticationEntryPoint(getAuthenticationEntryPoint());
    }

    protected void authorizeRequests(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
        val properties = getAuthProperties();
        val excludePaths = new ArrayList<>(properties.getExcludePath());

        excludePaths.add("/actuator/**");

        registry.antMatchers(excludePaths.toArray(new String[0])).permitAll()
            .anyRequest().hasRole(properties.getRole());
    }

    protected void addFilters(HttpSecurity http) {
        http.addFilterBefore(new GatewayResolveFilter(), BasicAuthenticationFilter.class);
    }

    @Nullable
    protected CorsConfigurationSource getCorsConfigurationSource() {
        return cors;
    }

    protected AuthenticationEntryPoint getAuthenticationEntryPoint() {
        return handler;
    }

    protected AccessDeniedHandler getAccessDeniedHandler() {
        return handler;
    }

    protected AuthProperties getAuthProperties() {
        return properties;
    }

    private static final class WrappedHandler implements AccessDeniedHandler, AuthenticationEntryPoint {

        private final ObjectMapper mapper;

        private final ProjectExceptionHandler handler;

        private WrappedHandler(ObjectMapper mapper, ProjectExceptionHandler handler) {
            this.mapper = mapper;
            this.handler = handler;
        }

        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
            handle0(request, response, new ResponseStatusException(HttpStatus.UNAUTHORIZED, e.getMessage(), e));
        }

        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException {
            handle0(request, response, new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage(), e));
        }

        private void handle0(HttpServletRequest request, HttpServletResponse response, ResponseStatusException e) throws IOException {
            val result = handler.handleError(request, e);

            response.setStatus(result.getStatusCodeValue());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("UTF-8");

            val writer = response.getWriter();

            mapper.writeValue(writer, result.getBody());
            writer.flush();
        }
    }
}
